﻿#region License
/**	
The MIT License (MIT)

Copyright (c) 2013 Philipp Schillinger, Michael Alekseew, Andreas Braun, Steeven Zeiß

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. 
 **/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Drawing;
using System.Threading;
using CLEyeMulticam;
using Emgu.CV.Structure;
using Emgu.CV;
using IndoorLocalization.Detection;

using System.Xml.Serialization;

namespace IndoorLocalization
{
	/// <summary>
	/// Represents a camera that is used to detect humans.
	/// </summary>
	[Serializable()]
	public class SensorCam : IDisposable,IDeserializationCallback {

		/// <summary>
		/// An event that is fired each time the frame of this camera is updated.
		/// </summary>
		[field: NonSerialized]
		public event EventHandler Updated;

		/// <summary>
		/// An event that is fired when this sensor cam is disposed.
		/// </summary>
		[field: NonSerialized]
		public event EventHandler Disposed;

		/// <summary>
		/// An event that is fired each time the name of this camera was changed.
		/// </summary>
		[field: NonSerialized]
		public event EventHandler NameChanged;

		/// <summary>
		/// An event that is fired each time the this camera is paused or resumend.
		/// </summary>
		[field: NonSerialized]
		public event EventHandler PausedStateChanged;

		/// <summary>
		/// The x position where this sensor is placed.
		/// </summary>
		public int X {
			get { return position.X; }
			set { position.X = value; }
		}
		/// <summary>
		/// The y position where this sensor is placed.
		/// </summary>
		public int Y {
			get { return position.Y; }
			set { position.Y = value; }
		}
		/// <summary>
		/// The orientation with which this sensor is placed.
		/// In degree between 0 and 359.
		/// </summary>
		public float Orientation { get; set; }
		/// <summary>
		/// The pitch with which this sensor is placed.
		/// In degree between -30 and 30.
		/// </summary>
		public double Pitch {
			get { return pitch; }
			set {
				pitch = value;
				UpdateTransMatrix();
			}
		}
		/// <summary>
		/// Returns the transformation matrix for the correction of the perspective view depending on the pitch.
		///    /   |   \    => transMatrix =>   |   |   |
		/// </summary>
		public HomographyMatrix TransMatrix { get { return transMatrix; } }
		/// <summary>
		/// The name of this camera, shown in the GUI.
		/// </summary>
		public String Name {
			get { return name; }
			set {
				name = value;
				// report event
				if (NameChanged != null) NameChanged(this, null);
			}
		}
		/// <summary>
		/// The current frame of this camera.
		/// </summary>
		public Image<Bgr, Byte> Frame { 
			get {
				lock (frame) return frame;
			}
		}
		/// <summary>
		/// The current frame of this camera transformed with the warp perspective view that depends on the pitch.
		/// </summary>
		public Image<Bgr, Byte> TransFrame {
			get {
				lock (transFrame) return transFrame;
			}
		}
		/// <summary>
		/// The current frame generated by motion detection.
		/// </summary>
		public Image<Bgr, Byte> MotionFrame {
			get {
				lock (motionFrame) return motionFrame;
			}
		}
        /// <summary>
        /// The current reference frame for motion detection.
        /// </summary>
        public Image<Gray, Byte> RefFrameGray {
            get { return refFrameGray; }
            set { lock(refFrameGray) refFrameGray = value; }
        }
		/// <summary>
		/// The current calculated motion frame.
		/// </summary>
		public Image<Gray, Byte> CalcFrameGray {
			get
			{
				lock (calcFrameGray) return calcFrameGray;
			}
		}
		/// <summary>
		/// The GUID of this sensor cam.
		/// </summary>
		public Guid GUID {
			get { return guid; }
			set {
				Guid oldGuid = guid;
				guid = value;
				// update the device lists
				if (!guid.Equals(Guid.Empty)) {
					if (device != null) {
						threadAlive = false;
						updateThread.Join();
						updateThread.Abort();
						Program.FreeDevice(oldGuid); // just need to free old device
					}
					// start new device
					// this method also calls UseDevice
					StartCam();
				}
			}
		}
		/// <summary>
		/// The merger to which detections will be reported.
		/// </summary>
		public SensorMerger Merger {
			get { return merger; }
			set {
				if(merger != null) merger.RemoveAllRaysTS(GUID);
				merger = value;
			}
		}
		/// <summary>
		/// The camera device which is used by this sensor.
		/// </summary>
		public CLEyeCameraDevice Device {
			get { return device; }
		}

		/// <summary>
		/// The name of this camera, shown in the GUI.
		/// </summary>
		private String name;
		/// <summary>
		/// The position where this sensor is placed.
		/// </summary>
		private HCoord position;
		/// <summary>
		/// The pitch with which this sensor is placed
		/// </summary>
		private double pitch;
		/// <summary>
		/// The transformation matrix for the correction of the perspective view depending on the pitch.
		/// </summary>
		private HomographyMatrix transMatrix;
		/// <summary>
		/// The merger to which detections will be reported.
		/// </summary>
		private SensorMerger merger;
		/// <summary>
		/// The camera device which is used by this sensor.
		/// </summary>
		[NonSerialized]
		private CLEyeCameraDevice device;
		/// <summary>
		/// The guid of the device.
		/// </summary>
		private Guid guid;
		/// <summary>
		/// The current frame of this camera.
		/// </summary>
		[NonSerialized]
		private Image<Bgr, Byte> frame;
		/// <summary>
		/// The current frame of this camera transformed with the warp perspective view that depends on the pitch.
		/// </summary>
		[NonSerialized]
		private Image<Bgr, Byte> transFrame;
		/// <summary>
		/// The current frame generated by motion detection.
		/// </summary>
		[NonSerialized]
		private Image<Bgr, Byte> motionFrame;
        /// <summary>
        /// The current reference frame for motion detection.
        /// </summary>
        [NonSerialized]
        private Image<Gray, Byte> refFrameGray;
		/// <summary>
		/// The current calculated motion frame.
		/// </summary>
		[NonSerialized]
		private Image<Gray, Byte> calcFrameGray;
		/// <summary>
		/// The thread that is used to run the update method.
		/// </summary>
		[NonSerialized]
		private Thread updateThread;
		/// <summary>
		/// Indicates whether the capture thread is alive or not.
		/// </summary>
		private bool threadAlive = false;
		/// <summary>
		/// Indicates whether the capture thread is paused or not.
		/// </summary>
		private bool paused;

		/// <summary>
		/// Called when the camera is deserialized.
		/// </summary>
		void IDeserializationCallback.OnDeserialization(Object sender) {
			this.frame = new Image<Bgr, Byte>(640, 480);
            this.motionFrame = new Image<Bgr, Byte>(640, 480);
            this.refFrameGray = new Image<Gray, Byte>(640, 480);
			UpdateTransMatrix();
		}

		/// <summary>
		/// Creates a new camera as sensor at the specified location and starts it.
		/// Detections of this sensor will be merged with all other sensors
		/// that use the same merger.
		/// </summary>
		/// <param name="x">X position of this camera.</param>
		/// <param name="y">Y position of this camera.</param>
		/// <param name="orientation">Orientation of this camera.</param>
		/// <param name="pitch">Pitch of this camera.</param>
		/// <param name="name">Name of this camera, used by the GUI.</param>
		/// <param name="guid">ID of the device that is used as camera.</param>
		/// <param name="merger">Merger that should be used to evaluate detections.</param>
		public SensorCam(int x, int y, float orientation, double pitch, String name, Guid guid, SensorMerger merger) {
			// initialize attributes
			position = new HCoord(x, y);
			this.Orientation = orientation;
			this.Pitch = pitch;
			this.Name = name;
			this.merger = merger;
			this.frame = new Image<Bgr, Byte>(640, 480);
            this.motionFrame = new Image<Bgr, Byte>(640, 480);
            this.refFrameGray = new Image<Gray, Byte>(640, 480);
			this.guid = guid;

			StartCam();
		}

        /// <summary>
        /// Dummy constructor for a new sensor cam. This cam is not able to run, but it can be used for calculation.
        /// </summary>
        /// <param name="x">X position of this camera.</param>
        /// <param name="y">Y position of this camera.</param>
        /// <param name="orientation">Orientation of this camera.</param>
		public SensorCam(int x, int y, float orientation) {
			position = new HCoord(x, y);
			this.Orientation = orientation;
		}

		/// <summary>
		/// Updates the transformation matrix for the correction of the perspective view depending on the current pitch.
		/// </summary>
		private void UpdateTransMatrix() {
			PointF[] srcPoints = new PointF[4];
			PointF[] destPoints = new PointF[4];

			int pixelDistance = (int)(Math.Abs(Pitch) * Program.PitchTransFactor);

			destPoints[0].X = 0;
			destPoints[0].Y = 0;
			destPoints[1].X = Program.FrameWidth;
			destPoints[1].Y = 0;
			destPoints[2].X = 0;
			destPoints[2].Y = Program.FrameHeight;
			destPoints[3].X = Program.FrameWidth;
			destPoints[3].Y = Program.FrameHeight;

			if (Pitch >= 0) {
				srcPoints[0].X = pixelDistance;
				srcPoints[0].Y = 0;
				srcPoints[1].X = Program.FrameWidth - pixelDistance;
				srcPoints[1].Y = 0;
				srcPoints[2].X = 0;
				srcPoints[2].Y = Program.FrameHeight;
				srcPoints[3].X = Program.FrameWidth;
				srcPoints[3].Y = Program.FrameHeight;
			} else {
				srcPoints[0].X = 0;
				srcPoints[0].Y = 0;
				srcPoints[1].X = Program.FrameWidth;
				srcPoints[1].Y = 0;
				srcPoints[2].X = pixelDistance;
				srcPoints[2].Y = Program.FrameHeight;
				srcPoints[3].X = Program.FrameWidth - pixelDistance;
				srcPoints[3].Y = Program.FrameHeight;
			}

			transMatrix = CameraCalibration.GetPerspectiveTransform(srcPoints, destPoints);
		}

		/// <summary>
		/// Creates the internal cam and starts the update thread.
		/// A valid GUID has to be set.
		/// This Method may be called during deserialization but never else.
		/// </summary>
		public void StartCam() {
			// set up the device
			device = Program.UseDevice(guid);

			// start update thread
			updateThread = new Thread(Update);
			updateThread.IsBackground = true;
			threadAlive = true;
			paused = false;
			updateThread.Start();
		}

		/// <summary>
		/// Disposes this sensor cam and aborts its capture thread.
		/// </summary>
		public void Dispose() {
			// fire disposed event
			if (Disposed != null) Disposed(this, null);

			// abort update thread and free device
			threadAlive = false;
			updateThread.Join();
			updateThread.Abort();
			Program.FreeDevice(guid);

			// remove own rays
			merger.RemoveAllRaysTS(guid);

			// free memory
			frame.Dispose();
		}

		/// <summary>
		/// Pause this sensor cam which means that it will not update its image
		/// and will stop searching for persons.
		/// </summary>
		public void Pause() {
			paused = true;
			if (PausedStateChanged != null) PausedStateChanged(this, null);
		}

		/// <summary>
		/// Resume this sensor cam which will cause it to work again when paused.
		/// If not paused, there is no effect.
		/// </summary>
		public void Resume() {
			paused = false;
			if (PausedStateChanged != null) PausedStateChanged(this, null);
		}

		/// <summary>
		/// Indicates if this sensor cam is currently in a paused state.
		/// </summary>
		/// <returns><c>true</c> if paused, otherwise <c>false</c>.</returns>
		public bool IsPaused() {
			return paused;
		}

		/// <summary>
		/// Endless loop to update this sensor and report detections to the merger.
		/// Should be started as a background thread.
		/// </summary>
		private void Update() {
			while (threadAlive) {
				// check if paused
				if (paused) {
					merger.RemoveAllRaysTS(GUID);
					Thread.Sleep(1000);
					continue;
				}
				// is a new frame ready?
				if (device != null && device.Grab()) {
					List<SensorRay> newRays = new List<SensorRay>();
					Image<Bgr, Byte> newFrame = device.RetrieveBgrFrame();
					lock (frame) {
						frame = newFrame;
					}
					lock (motionFrame) {
						motionFrame = newFrame;
					}

					// test ray (remove if implemented)
					//int angle = DateTime.Now.Second-30;
                    //int angle = -25;
					//newRays.Add(new SensorRay(angle, this));

					// detect motion
					newRays = MotionDetection.DetectMotion(this, ref transFrame, ref motionFrame, ref calcFrameGray);
					merger.UpdateRaysTS(GUID, newRays);

					if (Updated != null) Updated(this, null);
				}
			}
		}

	}
}
